<?php
/**
 * User class.
 * 
 * LICENSE:
 * 
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with This library. If not, see <{@link http://www.gnu.org/licenses/}>.
 *
 * @link http://code.google.com/p/phpstartapp/
 * @author Bojan Mauser <bmauser@gmail.com>
 * @copyright Bojan Mauser 2009
 * @package psa
 * @version $Id: Psa_User.class.php 464 2009-03-24 00:30:12Z bmauser $
 */


/**
 * User object class.
 * 
 * Here are some examples of using Psa_User object. You will mostly use Psa_User objects
 * in model methods. Here are parts of code that can be used in some model method:
 * 
 * <b>Examples</b>
 * 
 * <b>1)</b> Create a new user:
 * <code>
 * // user object
 * $user = new Psa_User('new');
 * 
 * // new user username
 * $user->username = 'new_user';
 * 
 * // new user password
 * $user->password = 'new_user_pass';
 * 
 * // save user to database
 * try{
 * 	$user->save();
 * 	echo "new user is created";
 * }
 * catch(Psa_User_Exception $e){
 * 	echo "error. user not created. Maybe already exists. See log for details.";
 * }
 * </code>
 * 
 * <b>2)</b> Authorize user by username & password and save some properties of the user:
 * <code>
 * // user object
 * $user = new Psa_User();
 * 
 * // set username & password
 * $user->username = 'john';
 * $user->password = 'fish';
 * 
 * // if user is authorized save some custom properties
 * if($user->authorize()){
 * 	$user->likes_to_eat = 'potato';
 * 	$user->likes_to_drink = array('cola','bear');
 * 	$user->save();
 * }
 * </code>
 * 
 * <b>3)</b> Extend <kbd>Psa_User</kbd> class and add new authorization method:
 * <code>
 * // My_Psa_User class
 * class My_Psa_User extends Psa_User{
 * 
 * 	function ldap_authorization($ldap_username,$ldap_password){
 * 		
 * 		// check LDAP authorization and set $ok_in_ldap flag
 * 		// ...
 * 
 * 		// if LDAP authorization passed
 * 		if($ok_in_ldap){
 * 			$this->username = $ldap_username;
 * 			return $this->authorize('username');
 * 		}
 * 		else
 * 			return 0;
 * 	}
 * }
 * </code>
 * 
 * Code in some model method:
 * <code>
 * // user object
 * $user = new My_Psa_User();
 * 
 * // authorize user. Users in the psa_user database table should have the same usernames like in ldap
 * if($user->ldap_authorization('My_ldap_username','My_ldap_pass')){
 * 
 * 	// save last login time
 * 	$user->save_last_login_time();
 * }
 * </code>
 * 
 * <b>4)</b> Change user's group membership and check some tags:
 * <code>
 * // user object
 * $user = new Psa_User(10);
 * 
 * // authorize user by id
 * $user->authorize('id');
 * 
 * // remove user from all user groups 
 * $user->remove_group('all');
 * 
 * // remove the user from group with ID 9 and put it in groups: 3,5 and 7
 * $user->remove_group(9);
 * $user->add_group(array(3,5,7));
 * 
 * // check if user has tag 'picture_upload'
 * if($user->has_tag('picture_upload')){
 * 	// do something
 * }
 * </code>
 * 
 * <b>Note:</b> Read more about the tags {@tutorial psa_features.pkg#usersgroups.tags here}.
 */
class Psa_User{

	/**
	 * User ID.
	 * Primary key from <kbd>psa_user</kbd> database table.
	 * 
	 * <b>Note:</b> You can set this property only if not set, otherwise you have only read access to this property.
	 * @var int
	 */
	protected $id;


	/**
	 * Username
	 * 
	 * @var string
	 */
	public $username;


	/**
	 * Password (not encrypted).
	 * You will need to set this property when you authorize user with username and password or when you
	 * creating a new user. This property will not be set after authorization with {@link authorize()} method. 
	 * 
	 * @var string
	 * @see password_change()
	 * @see password_encrypt()
	 */
	public $password;


	/**
	 * Array with user tags.
	 * Always check user's tags with {@link has_tag()} method because it will refresh this array if
	 * user's group membership has changed.
	 * This array is set with {@link set_tags()} method. 
	 * Array structure is same as {@link Psa_Group::$tags $tags} 
	 * array in {@link Psa_Group} object. The difference is that this one contains enabled tag from
	 * all groups user is in. Indexes in this array are tags (tag names) that are set for the user. 
	 * All elements will have value 1.
	 * Read more about the tags {@tutorial psa_features.pkg#usersgroups.tags here}.
	 * 
	 * Example:<pre>
	 * Array
	 * (
	 *     ['address_book'] => 1
	 *     ['download_file'] => 1  
	 *     ['view_source'] => 1  
	 * )
	 * </pre>
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var array
	 * @see set_tags()
	 * @see has_tag()
	 * @see Psa_Group::$tags
	 */
	protected $tags = array();	


	/**
	 * Array with all groups user is in.
	 * This array is set with {@link set_tags()} method.
	 * 
	 * Example:<pre>
	 * Array
	 * (
	 *     [11] => 'admins'  // array index is group id and value is group name
	 *     [14] => 'users' 
	 *     [33] => 'other' 
	 * )
	 * </pre>
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var array
	 * @see set_tags()
	 */
	protected $groups = array();
	

	/**
	 * UNIX timestamp of time user has last logged in.
	 * Last login time has to be saved with {@link save_last_login_time()} method.
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var int
	 * @see save_last_login_time()
	 */
	protected $last_login = 0;
	
	
	/**
	 * Flag for new user creation.
	 * It is set to 1 from constructor method if argument is 'new'.
	 * 
	 * @var int
	 * @ignore 
	 */
	protected $new_user = 0;	
	
	
	/**
	 * Database connect object
	 * 
	 * @var Psa_Pdo
	 * @ignore 
	 */
	protected $psa_database;
	
	
	/**
	 * Flag if user is authorized.
	 * If {@link authorize()} method returns true it is set to 1.
	 * 
	 * <b>Note:</b> You have read access to this property.
	 * 
	 * @var int
	 */
	protected $authorized = 0;
	
	
	/**
	 * Flag if user object is saved to session with {@link session_save()} method.
	 * 
	 * @see session_save()
	 * @var string 
	 * @ignore 
	 */
	protected $saved_to_session = null;
	
	
	/**
	 * Flag if tags set for the user.
	 * It is set from set_tags() method.
	 * 
	 * @var int
	 * @ignore 
	 */
	protected $tags_are_set = 0;

	
	/**
	 * Array that holds value of properties from custom columns added to psa_user table.
	 * @var array
	 * @ignore 
	 */
	protected $orm_data = array();
	
	
	/**
	 * Array that holds references to values that are changed in $orm_data array.
	 * This is used to decide which orm properties to update in database.
	 * @var array
	 * @ignore 
	 */
	protected $orm_data_changed = array();
	
	

	/**
	 * Constructor method.
	 * Can be called with three types of arguments:
	 * - If called with '<kbd>new</kbd>' as the argument new user will be created when {@link save()} 
	 *   method is called.
	 * - If called with user ID as argument, objects property {@link $id} is set. In this case 
	 *   <var>$user_id</var> must be an integer value so be sure to pass integer as the argument 
	 *   and use cast operator <kbd>(int)</kbd>.
	 * - If <var>$user_id</var> parameter is string (different from '<kbd>new</kbd>'), 
	 *   the {@link $username} property is set.
	 * 
	 * @param int|string $user_id '<kbd>new</kbd>' if new user should be created or ID or username of the user
	 */
	public function __construct($user_id = null){
		
		// global database connect object
		global $psa_database;
		
		// reference to global database object
		$this->psa_database = $psa_database;
		
		// Set $new_user property. Flag that new user creation process is started.
		if($user_id === 'new'){
			$this->new_user = 1;
			// find if we have some orm data
			$this->restore_new_user();
		}
		// set $id object property
		else if($user_id && is_int($user_id))
			$this->id = $user_id;
		else 
			$this->username = $user_id;
	}


	/**
	 * Authorizes the user.
	 * Checks if user exists and loads data about the user from the database and restores object properties 
	 * previously saved with {@link save()} method.
	 * See examples in {@link Psa_User} class description.
	 * 
	 * There are three authorization types:
	 * - If <var>$type</var> argument is '<kbd>id</kbd>' it sets the user data (if exists in database) 
	 *   without checking username and password. {@link $id} object property must be set before for
	 *   this authorization type.
	 * - If <var>$type</var> argument is '<kbd>username</kbd>' it authorizes the user (if exists in database) and sets user data 
	 *   without checking username and password. {@link $username} object property must be set before 
	 *   for this authorization type.
	 * - If <var>$type</var> argument is not set {@link $username} and {@link $password} properties 
	 *   are required to be set.
	 * 
	 * <i>id</i> and <i>username</i> authorization types can be useful, for example, if you store only user
	 * id in session or want to switch user in your application or usernames and passwords are stored 
	 * in external source like LDAP.
	 * 
	 * @param string $type '<kbd>id</kbd>' for authorization only by user ID, null (default) 
	 * for authorization with username and password, '<kbd>username</kbd>' for authorization only 
	 * by username
	 * @param bool $run_plugins if false Psa_Plugin_After_User_Authorize plugins will not be invoked
	 * @return int 1 for success, 0 if not authorized
	 */
	public function authorize($type = null, $run_plugins = true){
		
		// global config array
		global $PSA_CFG;
		
		// default authorization. Requires $this->username and $this->password to be set
		if(!$type && $this->username && $this->password){
			
			// passwords in the database are hashed
			$pass_in_database = $this->password_encrypt($this->password);
			
			// find user in the database
			$sql = "SELECT *,UNIX_TIMESTAMP(psa_last_login) AS psa_login_timestamp FROM {$PSA_CFG['database']['table']['user']} WHERE psa_username = " . $this->psa_database->escape($this->username) . " AND psa_password = " . $this->psa_database->escape($pass_in_database);
		}
		// authorize user with id only
		else if($type == 'id' && $this->id){
			
			// find user in the database
			$sql = "SELECT *,UNIX_TIMESTAMP(psa_last_login) AS psa_login_timestamp FROM {$PSA_CFG['database']['table']['user']} WHERE psa_id = " . $this->psa_database->escape($this->id);
		}
		// authorize user with username only
		else if($type == 'username' && $this->username){
			
			// find user in the database
			$sql = "SELECT *,UNIX_TIMESTAMP(psa_last_login) AS psa_login_timestamp FROM {$PSA_CFG['database']['table']['user']} WHERE psa_username = " . $this->psa_database->escape($this->username);
		}
		else{
			$this->log("Username or password or id not set or unknown authorization type. Maybe you use 'id' type but you did not pass integer argument to constructor.",__METHOD__,1,'warning');
			return 0;
		}
		
		// run query against the database
		$this->psa_database->query($sql);
		
		// All data about the user. Entire row from database table
		$user_data = $this->psa_database->fetch_row();
		
		// if row in the database table exists (username and password are ok or user with id exists)
		if(isset($user_data['psa_id'])){
				
			// set objects properties
			$this->restore($user_data);

			// write log
			$this->log("User authorized",__METHOD__,2);
			
			// set flag that user is authorized
			$this->authorized = 1;
			
			// run Psa_Plugin_After_User_Authorize plugins
			if($run_plugins)
				psa_run_plugins(array('Psa_Plugin_After_User_Authorize' => array('psa_main' => array($this))),'by_type','no_unregistered_warning');
			
			return 1;
		}
		// user does not exists or password is wrong
		else{
			// write log
			$this->log("User not authorized. User does not exists or invalid password.",__METHOD__,2);
		}
		
		return 0;
	}


	/**
	 * Sets (restores) users properties previously saved with the {@link save()} method.
	 * This method is intended to be called from the {@link authorize()} method.
	 * 
	 * Example of $user_data argument:<pre>
	 * Array
	 * (
	 *     [psa_id] => 1              // user id
	 *     [psa_username] => user1    // username
	 *     [psa_serialized_data] =>   // entire Psa_User object serialized with serialize() PHP function
	 * )
	 * </pre>
	 * 
	 * @param array $user_data associative array with data from the user database table.
	 * @see save()
	 * @ignore 
	 */
	protected function restore($user_data){
		
		// if array is not empty
		if($user_data){
			
			if($user_data['psa_serialized_data']){
				
				// load all saved properties from saved object in the database
				$serialized_object = unserialize(base64_decode($user_data['psa_serialized_data']));
				
				if(is_object($serialized_object)){
					// unset some properties to not override them in current user object
					$this->serialize_clean_obj($serialized_object);
					
					// set current objects properties
					foreach ($serialized_object as $serialized_object_key => $serialized_object_value) {
						$this->$serialized_object_key = $serialized_object_value;
					}
				}
			}
			
			// check if there are some custom table rows in pas_user database table
			if(sizeof($user_data) > 6){
				$this->set_orm_data($user_data);
			}
			
			// set objects properties from databse
			$this->id = (int)$user_data['psa_id'];
			$this->username = $user_data['psa_username'];
			$this->last_login = $user_data['psa_login_timestamp'];
		}
	}

	
	/**
	 * Checks if there are some custom table rows in pas_user database table.
	 * It finds out all columns from database and fills $orm_data array.
	 * @ignore 
	 */
	protected function restore_new_user(){
		
		global $PSA_CFG;
		
		// We need one dummy query here to get all column names.
		// This info can be also fetched form SCHEMA database with query like this:
		// SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '{$PSA_CFG['database']['table']['user']}' AND table_schema = '{$PSA_CFG['pdo']['database']}'
		// That is maybe faster, especially if there if is there is a lot of data inside fetched row.
		$sql_dummy = "SELECT * FROM {$PSA_CFG['database']['table']['user']} LIMIT 1";
		
		$this->psa_database->query($sql_dummy);
		
		// All data about the user. Entire row from database table
		$sql_dummy_user_data = $this->psa_database->fetch_row();
		
		if(sizeof($sql_dummy_user_data) > 5){
			$this->set_orm_data($sql_dummy_user_data,1);
		}
	}
	

	/**
	 * Saves all custom properties of the user object to the database.
	 * In your application you can add some custom properties to user object, or add some properties by 
	 * extending user class and save those values to the database with this method.
	 * All saved properties will be restored when the {@link authorize()} method is called and is successful.
	 * See <kbd>serialize()</kbd> PHP function for details what you can save.
	 * See examples in {@link Psa_User} class description. Throws {@link Psa_User_Exception} on error.
	 * 
	 * @see authorize()
	 * {@internal @see restore()}}
	 * @return int 1 for success
	 * @throws Psa_User_Exception
	 */
	public function save(){
		
		if($this->authorized or ($this->new_user && $this->username && $this->password)){
			
			// global config array
			global $PSA_CFG;
			
			// save only relevant data
			// I guess that __sleep() and __restore() methods can be more efficient here but I had some problems with them... so we cloning
			$temp_obj = clone $this;
			
			// unset unneeded data. Store only needed data in database.
			$this->serialize_clean_obj($temp_obj);
			
			// serialize object. I had to use base64_encode due to http://bugs.php.net/bug.php?id=44433 with pdo_pgsql
			$serialized_data = base64_encode(serialize($temp_obj));
			
			// if this is new user creation
			if($this->new_user){
				
				// check if there are some custom table rows in psa_user database table
				$custom_columns_insert = psa_orm_make_query_parts(2, $this->orm_data_changed);			
				$password = $this->password_encrypt($this->password);
				$sql = "INSERT INTO {$PSA_CFG['database']['table']['user']} (psa_serialized_data, psa_username, psa_password" .$custom_columns_insert['insert_query_columns']. ") VALUES (?, ?, ?" . $custom_columns_insert['insert_query_values'] . ')';
				$query_params = array_merge(array($serialized_data, $this->username, $password),$custom_columns_insert['values']);
			}
			// update existing user in the database
			else{
				// check if there are some custom table rows in psa_user database table
				$custom_columns_update = psa_orm_make_query_parts(1, $this->orm_data_changed);
				$sql = "UPDATE {$PSA_CFG['database']['table']['user']} SET psa_serialized_data = ?, psa_username = ?" . $custom_columns_update['update_query_set'] . " WHERE psa_id = ?";	
				$query_params = array_merge(array($serialized_data, $this->username),$custom_columns_update['values']);
				$query_params[] = $this->id;
			}
			
			// run Psa_Plugin_Before_User_Create plugins
			if($this->new_user){
				psa_run_plugins(array('Psa_Plugin_Before_User_Create' => array('psa_main' => array($this))),'by_type','no_unregistered_warning');
			}
			
			try{
				// run query against the database
				$this->psa_database->execute($query_params,$this->psa_database->prepare($sql));
				
				// set the id value of object if new user is created
				if($this->new_user){
					
					// set the id of new user
					$this->id = (int)$this->psa_database->last_insert_id('psa_user_id_seq');
					
					// turn off new_user flag
					$this->new_user = 0;
					
					// set flag that user is authorized
					$this->authorized = 1;
					
					// run Psa_Plugin_After_User_Create plugins
					psa_run_plugins(array('Psa_Plugin_After_User_Create' => array('psa_main' => array($this))),'by_type','no_unregistered_warning');
					
					// for log
					$log_message  = "New user created";
					
					// reload data for new user
					if(count($this->orm_data))
						$this->authorize('id',false);
				}
				else{
					// for log
					$log_message  = "User data saved";
				}
				
				// all orm properties are saved now so reset changes tracker
				$this->orm_data_changed = array();
				
				// if there is data in session update session.
				if($this->saved_to_session){
					$this->session_save();
				}
				
				// write log
				$this->log($log_message,__METHOD__,2);
				
				return 1;
			}
			catch (Psa_Db_Exception $e){
				include_once 'Psa_User_Exception.class.php';
				throw new Psa_User_Exception("Error saving user data to database", 20);
			}
		}
		else{
			include_once 'Psa_User_Exception.class.php';
			throw new Psa_User_Exception("Error saving user data to database. User not authorized or username & password not set for a new user", 21);
		}
	}


	/**
	 * Puts user in a group or more groups if the <var>$group_id</var> argument is array.
	 * Group membership changes are immediately stored in the database and you don't have to 
	 * call {@link save()} method after.
	 * If you creating a new user this method must be called after {@link save()} method.
	 * Throws {@link Psa_User_Exception} on error.
	 * 
	 * Example:
	 * <code>
	 * // user object
	 * $user = new Psa_User();
	 * 
	 * // set user ID and authorize user
	 * $user->id = 123;
	 * if($user->authorize('id'))
	 * 	// put user in groups that have IDs 4,12 and 33
	 * 	$user->add_group(array(4,12,33));
	 * </code>
	 * 
	 * {@internal This is wrapper method for add_remove_group() method.}}
	 * @param int|array $group_id id of the group. Or array with groups ids.
	 * @return int 1 for success, -1 user already was in the group (or more groups) 
	 * or group does not exists
	 * @see remove_group()
	 * @throws Psa_User_Exception
	 * {@internal @see add_remove_group()}}
	 */
	public function add_group($group_id){
		return $this->add_remove_group($group_id,1);
	}
	
	
	/**
	 * Remove user from a group or more groups if $group_id argument is array.
	 * Group membership changes are immediately stored in the database and you don't have to 
	 * call {@link save()} method after.
	 * If you creating a new user this method must be called after {@link save()} method.
	 * Throws {@link Psa_User_Exception} on error.
	 * This method calls {@link set_groups()} method after group membership is changed
	 * 
	 * Example:
	 * <code>
	 * // user object
	 * $user = new Psa_User();
	 * 
	 * // set user ID and authorize user
	 * $user->id = 123;
	 * if($user->authorize('id'))
	 * 	// remove user from groups that have ID 4
	 * 	$user->remove_group(4);
	 * </code>
	 * 
	 * {@internal This is wrapper method for add_remove_group() method.}}
	 * @param int|array|string $group_id id of the group or array with group ids. '<kbd>all</kbd>' 
	 * for remove user from all groups.
	 * @return int 1 for success, -1 user was not in the group (or more groups) 
	 * or group does not exists
	 * @see add_group()
	 * @throws Psa_User_Exception
	 * {@internal @see add_remove_group()}}
	 */
	public function remove_group($group_id){
		return $this->add_remove_group($group_id,0);
	}
	
	
	/**
	 * Puts user in the group or removes user from group (or more groups if $group_id argument is array).
	 * This method is called from add_group() and remove_group() methods.
	 * 
	 * @param int|array|string $group_id id of the group or array with group ids.
	 * @param int $action 1 add, 0 remove
	 * @return int 1 for success, -1
	 * @see add_group()
	 * @see remove_group()
	 * @throws Psa_User_Exception
	 * @ignore 
	 */
	protected function add_remove_group($group_id,$action){
		
		if($this->authorized && $group_id){
			
			// global config array
			global $PSA_CFG;
			
			// if $group_id is not array make it array for foreach loop
			if(!is_array($group_id))
				$group_id = array($group_id);
			
			// flag if some query failed in the foreach loop
			$success = $failed = 0;
				
			// add user to one or more groups or remove from group(s)
			foreach ($group_id as $group_id_key => &$group_id_value){
				
				// add to group
				if($action == 1){
					$sql = "INSERT INTO {$PSA_CFG['database']['table']['user_in_group']} (user_id, group_id) VALUES ('{$this->id}', '{$group_id_value}')";
				}
				// remove from group
				else if($action == 0){
					
					// remove all groups
					if($group_id_value == 'all')
						$sql = "DELETE FROM {$PSA_CFG['database']['table']['user_in_group']} WHERE user_id = '{$this->id}'";
					// remove specific group
					else
						$sql = "DELETE FROM {$PSA_CFG['database']['table']['user_in_group']} WHERE user_id = '{$this->id}' AND group_id='{$group_id_value}'";
				}
				
				// run query against the database
				try{
					$this->psa_database->query($sql);
				}
				catch(Psa_Db_Exception $e){
					$failed = 1;
				}
				
				// if no rows affected user already wasn't or was in the group depending on $action
				if($this->psa_database->affected_rows() <= 0)
					$failed = 1;
				else{
					// set flag that tags are not set cause group membership is changed
					$this->tags_are_set = 0;
					
					$success = 1;
				}
			}
						
			if($success){
				
				// write log
				if($PSA_CFG['logging']['max_log_level'] >= 2)
					$this->log("Group membership changed: " . implode(',',$group_id) . " action=" . ($action ? 'add' : 'remove'),__METHOD__,2);
				
				// set tags if group membership is changed
				$this->set_tags();
				
				if(!$failed)
					return 1;
			}
			
			return -1;
		}
		
		include_once 'Psa_User_Exception.class.php';
		throw new Psa_User_Exception('Error changing user group. User not authorized or invalid $group_id method parameter.', 22);
	}
	
	
	/**
	 * Changes user password.
	 * New password will be hashed with {@link password_encrypt()} method and stored in the database.
	 * When creating a new user you don't have to call this method, just set $password property.
	 * You need to call it only when you want to change password for existing user. 
	 * Throws {@link Psa_User_Exception} on error.
	 * 
	 * @param string $new_password new user password
	 * @return int 1 for success
	 * @throws Psa_User_Exception
	 * @see password_encrypt()
	 */
	public function password_change($new_password){
		
		if($this->authorized && $new_password){
			
			// global config array
			global $PSA_CFG;
			
			// encrypt new password
			$new_password = $this->password_encrypt($new_password);
			
			// update user in the database
			$sql = "UPDATE {$PSA_CFG['database']['table']['user']} SET psa_password = '$new_password' WHERE psa_id = '{$this->id}'";
			
			// run query against the database
			try{
				$this->psa_database->query($sql);
				
				// write log
				$this->log("Password changed",__METHOD__,2);
				
				return 1;
			}
			catch (Psa_Db_Exception $e){
				
				include_once 'Psa_User_Exception.class.php';
				throw new Psa_User_Exception('Error changing password', 23);
			}
		}
		
		include_once 'Psa_User_Exception.class.php';
		throw new Psa_User_Exception('Error changing password. $password property not set or user not authorized', 24);
	}


	/**
	 * Verifies user password.
	 * Checks if given string is user's password. This method can be used in process of changing user password to
	 * verify old password. Throws {@link Psa_User_Exception} on error.
	 * 
	 * @param string $password password
	 * @return int 1 given password is valid, 0 given password is invalid
	 * @see password_encrypt()
	 * @see password_change()
	 */
	public function password_verify($password){
		
		if($this->authorized && $password){
			
			// global config array
			global $PSA_CFG;
			
			// serialize object
			$database_password = $this->password_encrypt($password);
			
			// update user in the database
			$sql = "SELECT psa_id FROM {$PSA_CFG['database']['table']['user']} WHERE psa_password = '$database_password' AND psa_id = '{$this->id}'";
			
			// run query against the database
			try{
				$this->psa_database->query($sql);
				
				$row = $this->psa_database->fetch_row();
				
				if(isset($row['psa_id']) && $row['psa_id'])
					return 1;
				else
					return 0;
				
			}
			catch (Psa_Db_Exception $e){
				
				include_once 'Psa_User_Exception.class.php';
				throw new Psa_User_Exception('Error verifying password', 25);
			}
		}
		
		return 0;
	}
	
	
	/**
	 * Encrypts user password. Password encrypted with this method will be stored in the database.
	 * It uses <kbd>hash()</kbd> PHP function. The hash type is set by <kbd>$PSA_CFG['password_hash']</kbd> 
	 * value in <kbd>config.php</kbd> file. By default is sha256.
	 * Override this method by extending {@link Psa_User} class if you want to change password hashing 
	 * function.
	 * 
	 * @param string $password password
	 * @see config.php
	 * @see password_change()
	 * @return string hashed (encrypted) password
	 */
	public function password_encrypt($password){
		
		// global config array
		global $PSA_CFG;
		
		// return hash
		return hash($PSA_CFG['password_hash'], $password);	
	}
	

	/**
	 * Unsets some properties from user object that we do not want to save in the database.
	 * This is helper function for restore() and save() functions.
	 * 
	 * @param object $user_object
	 * @param boolean $for_session if this is cleanup for session serialization
	 * @ignore 
	 */
	protected function serialize_clean_obj($user_object,$for_session = 0){
		
		if(is_object($user_object)){
			// for session
			if($for_session){
				unset($user_object->psa_database, $user_object->password);
			}
			// for database
			else{
				unset($user_object->orm_data, $user_object->orm_data_changed, $user_object->saved_to_session, $user_object->id, $user_object->password, $user_object->username, $user_object->psa_database, $user_object->new_user, $user_object->authorized, $user_object->tags_are_set, $user_object->tags);
			}
		}
	}
	
	
	/**
	 * Stores user object into PHP session or returns serialized representation of the user object.
	 * This is useful if you want to store complete user object and to restore it on next request without
	 * need to query the database.
	 * If you store object into session with this method, session variable that holds 
	 * serialized data will be updated on every call to {@link save()} method.
	 * The name of session variable will be <kbd>psa_user_serialized</kbd>.
	 * Also, in all log messages will contain <kbd>user_id</kbd> and <kbd>username</kbd> from this session
	 * variable. See {@link Psa_Logger::log()} method for details.
	 * Throws {@link Psa_User_Exception} if session is not started before calling this method.
	 * 
	 * @param bool $only_return if true, only serialized string will be returned.
	 * @return int|string
	 * @see session_restore()
	 */
	public function session_save($only_return = null){
		
		// check if session is started
		if(!session_id()){
			include_once 'Psa_User_Exception.class.php';
			throw new Psa_User_Exception('Session is not started. Cannot store Psa_User object into session.', 26);
		}
		
		$this->saved_to_session = 1;
		
		// clone object
		$temp_obj = clone $this;
			
		// unset unneeded data. Save only needed data.
		$this->serialize_clean_obj($temp_obj,'for_session');
		
		if(!$only_return){
			$_SESSION['psa_user_serialized'] = $temp_obj;
			return 1;
		}
		else{
			return serialize($temp_obj);
		}
	}
	
	
	/**
	 * Restores objects properties from session previously saved with {@link session_save()} method.
	 * PHP sessions have to be started with <kbd>session_start()</kbd> function before calling this method.
	 * 
	 * @return int 0 if session value <kbd>$_SESSION['psa_user_serialized']</kbd> is not set or its value 
	 * is not Psa_User object. 1 for successful restore.
	 * @see session_save()
	 */
	public function session_restore(){
		
		if(isset($_SESSION['psa_user_serialized'])){
			$serialized_object = $_SESSION['psa_user_serialized'];
			
			if(!($serialized_object instanceof Psa_User))
				return 0;
		}
		else
			return 0;
			
		// unset some properties to not override them in current user object
		$serialized_object->serialize_clean_obj($serialized_object,'for_session');
		
		// set current objects properties
		if(is_object($serialized_object)){
			foreach ($serialized_object as $serialized_object_key => $serialized_object_value) {
				$this->$serialized_object_key = $serialized_object_value;
			}
		}
		
		return 1;
	}
	
	
	/**
	 * Handles logging for this class.
	 * It calls log() method of Psa_Logger object.
	 * 
	 * @param string $message log message
	 * @param string $method class method which writes log message
	 * @param int $level log level
	 * @param string $type 'general', 'error', 'warning' ...
	 * @see Psa_Logger::log()
	 * @ignore 
	 */
	protected function log($message, $method = '', $level = 1, $type = ''){
		
		// global config array
		global $PSA_CFG;
		
		// if logging is enabled
		if($PSA_CFG['logging']['max_log_level'] >= $level){
			
			// parameters for Psa_Logger::log() method
			$log_data['user_id']  = $this->id;
			$log_data['username'] = $this->username;
			$log_data['message']  = $message;
			$log_data['function'] = $method;
			$log_data['level']    = $level;
			$log_data['type']     = $type;
			Psa_Logger::get_instance()->log($log_data);
		}
	}
	
	
	/**
	 * Fills {@link $tags} array with all tags for the user.
	 * It collects all tags from all groups the user is in.
	 *  
	 * It also sets {@link $groups} array with all groups user is in.
	 * This method is automatically called by {@link has_tag()} method if user's tags
	 * are not set or user's group membership has changed.
	 * 
	 * If some group tag has been changed (with {@link Psa_Group::add_tag() add_tag()} or
	 * {@link Psa_Group::remove_tag() remove_tag()} methods), and that change affects the user 
	 * (user is in that group), you have to call this method again if you want to update user's tags.
	 * 
	 * You shouldn't call this method on every web request (through {@link has_tag()} method also)
	 * if your users are in many groups. Instead you can use {@link session_save()} method to store 
	 * complete user object into session and to restore it on next request with {@link session_restore()}
	 * method.
	 * 
	 * Read more about the tags {@tutorial psa_features.pkg#usersgroups.tags here}.
	 * 
	 * @return int 1-tags are set
	 * @throws Psa_User_Exception
	 * @see $tags
	 * @see has_tag()
	 */
	public function set_tags(){
		
		// is user authorized
		if($this->authorized){
			
			// global config array
			global $PSA_CFG;
			
			// get data from all groups user is in
			$sql = "SELECT psa_user_in_group.group_id, psa_group.psa_serialized_data, psa_group.psa_name FROM psa_user_in_group LEFT JOIN psa_group ON psa_user_in_group.group_id = psa_group.psa_id WHERE psa_user_in_group.user_id = '{$this->id}'";
			
			try{
				$this->psa_database->query($sql);
			
				// for each fetched row
				while($row = $this->psa_database->fetch_row()){
					
					// set array ith all user groups
					$this->groups[$row['group_id']] = $row['psa_name'];
					
					// group object
					$temp_group_data = unserialize(base64_decode($row['psa_serialized_data']));
					
					// sum tags from all groups
					if(is_object($temp_group_data) && isset($temp_group_data->tags))
						$this->tags = array_merge($this->tags,$temp_group_data->tags);
				}
				
				// unset last serialized group object
				unset($temp_group_data);
				
				// set flag that tags are set
				$this->tags_are_set = 1;
				
				return 1;
			}
			catch (Psa_Db_Exception $e){
				include_once 'Psa_User_Exception.class.php';
				throw new Psa_User_Exception('Error with setting user tags. Cannot get tags from database', 27);
			}
		}
		
		include_once 'Psa_User_Exception.class.php';
		throw new Psa_User_Exception('Error with setting user tags. User not authorized', 28);
	}
	
	
	/**
	 * Sets {@link $groups} array with all groups user is in.
	 * This is alias for {@link set_tags()} method
	 * @see set_tags()
	 * @return int 
	 */
	public function set_groups(){
		return $this->set_tags();
	}
	
	
	/**
	 * Checks if user has the given tag.
	 * It calls {@link set_tags()} method if tags are not set before.
	 * It just checks if array element with index <var>$tag_id</var> exists in the {@link $tags} array.
	 * Read more about the tags {@tutorial psa_features.pkg#usersgroups.tags here}.
	 * 
	 * @param int|string $tag_id tag id (name)
	 * @return int 1-user has tag, 0-user hasn't tag, -1 error checking for tag
	 * @see set_tags()
	 * @see $tags
	 */
	public function has_tag($tag_id){
		
		// check if tags are set and call set_tags() method if not
		if(!$this->tags_are_set)
			$this->set_tags();
		
		if($this->tags_are_set){
			
			// if user is in some group that has all tags
			if(@$this->tags['psa_all'])
				return 1;
			else if(@$this->tags[$tag_id])
				return 1;
			else
				return 0;
		}
		
		// write log
		$this->log('Cannot check for tag. Maybe tags not set with set_tags() method.',__METHOD__,1,'error');
		
		return -1;
	}

	
	/**
	 * Sets last login time value for the user to current time.
	 * You need to call this method when you want to set last login time for the user.
	 * This method is not automatically called from the {@link authorize()} method because 
	 * every authorization doesn't mean that user has logged in. For example, you can authorize 
	 * some user just to check if exists.
	 * Throws {@link Psa_User_Exception} on error.
	 * 
	 * @return int 1-success
	 * @throws Psa_User_Exception
	 * @see $last_login
	 */
	public function save_last_login_time(){
		
		// check if tags are set and call set_tags() method if not
		if($this->id){
			
			// global config array
			global $PSA_CFG;
			
			// update user in the database
			$sql = "UPDATE {$PSA_CFG['database']['table']['user']} SET psa_last_login = now() WHERE psa_id = '{$this->id}'";
			
			// run query against the database
			try{
				$this->psa_database->query($sql);
				
				// set $last_login propery also to now(). 
				// This may not be exactly the same time as saved in the database due to query execution time.
				$this->last_login =  time();
			
				return 1;
			}
			catch (Psa_Db_Exception $e){
				include_once 'Psa_User_Exception.class.php';
				throw new Psa_User_Exception('Error saving last login time to database.', 29);
			}
		}
		else{
			include_once 'Psa_User_Exception.class.php';
			throw new Psa_User_Exception('Cannot save last login time. User id not set.', 201);
		}
	}
	
	
	/**
	 * Fills $this->orm_data and sets values of object properties from orm database columns.
	 * 
	 * @param array $user_data 'SELECT *' row from psa_user table
	 * @param bool $set_nulls if true values in $this->orm_data will be filled with nulls. 
	 * This is for creation of new users.
	 * @ignore 
	 */
	protected function set_orm_data(&$user_data,$set_nulls = 0){
		
		foreach ($user_data as $user_data_key => $user_data_value) {
			if(substr($user_data_key,0,4) != 'psa_'){
				
				if($set_nulls)
					$user_data_value = null;
				
				$this->orm_data[$user_data_key] = $user_data_value;
			}
		}
	}
	

	/**
	 * @ignore
	 */
	public function __set($name, $value) {
		
		// check if id is set and set it if not
		if($name == 'id' && !$this->id){
			$this->id = (int)$value;
		}
		// check if orm property should be set
		else if(array_key_exists($name,$this->orm_data)){
			$this->orm_data[$name] = $value;
			$this->orm_data_changed[$name] = &$this->orm_data[$name];
		}
		// if some property is not set set it
		else if(!isset($this->$name)){
			$this->$name = $value;
		}
		// show error
		else{
			$trace = debug_backtrace();
			trigger_error('Cannot set protected property: ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_ERROR);
			return null;
		}
	}
		
	
	/**
	 * @ignore
	 */
	public function __get($name){
		
		// enable to get orm properties
		if (array_key_exists($name, $this->orm_data)) {
			return $this->orm_data[$name];
		}
		
		// Makes some properties read-only
		if($name == 'authorized' or $name == 'tags' or $name == 'id' or $name == 'groups' or $name == 'last_login'){
			return $this->$name;
		}
		
		$trace = debug_backtrace();
		trigger_error('Undefined object property: ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
		return null;
	}
	
	
	/**
	 * @ignore
	 */
	public function __isset($name) {
		
		if(isset($this->$name) or isset($this->orm_data[$name]))
			return true;
			
		return false;
	}
	
}

